from app.extensions import db,login_manager # 从app文件夹里extensions.py中导入db对象和login_manager对象
from flask import current_app
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from werkzeug.security import generate_password_hash,check_password_hash
from flask_login import UserMixin # 提供了用户状态的获取与判断，类继承后对象就可以使用
from datetime import datetime


class User(db.Model,UserMixin):
    id=db.Column(db.Integer,primary_key=True,autoincrement=True) # autoincrement写不写都行
    username=db.Column(db.String(32),unique=True,nullable=False) # nullable不设置时默认为True
    password_hash=db.Column(db.String(128),nullable=False) # 加密后的密码94位。注意注册时创建User对象和修改密码时用password而不是password_hash，详情在下面装饰器上一行的注释
    email=db.Column(db.String(64),unique=True,nullable=False)
    phone=db.Column(db.String(11),unique=True,nullable=False)
    register_time=db.Column(db.DateTime,default=datetime.utcnow) # 注册时间，数据库中一般用utc时间(比东八区晚8小时，前端显示时要加8小时)，用本地时间时是datetime.now。注意utcnow后不加括号
    is_activated=db.Column(db.Boolean,default=False) # 是否已激活
    is_deleted=db.Column(db.Boolean,default=False) # 是否已注销
    portrait_file_name=db.Column(db.String(64),default='noavatar_middle.gif') # 头像文件名。未上传头像时使用noavatar_middle.gif作为默认头像。迁移前已有的数据，迁移后拥有默认值的字段的值为null；迁移后新增的数据，默认值生效。
    articles=db.relationship('Article',backref='user',lazy='dynamic') # 添加反向引用，第一个参数是关联的模型名，第二个参数是反向引用时的名字，即根据文章反向找作者时用文章.user，第三个参数是根据用户正向找文章时的加载时机。
    collections_and_likes=db.relationship('Article',secondary='collect_and_like',backref=db.backref('users',lazy='dynamic'),lazy='dynamic') # 收藏和点赞的反向引用，通过secondary指定使两表(user表和article表)关联的表，db.backref()里的lazy是根据文章id查收藏此文章的人的id时使用，最后的lazy是根据用户id查他收藏的文章的id时使用

    # 可能是设置了这个装饰器的原因，密码实际有2个，一个password因有装饰器而不可见，并通过password生成password_hash再赋给对象(即装饰器password.setter)，另一个password_hash在数据库。如果把这两个装饰器里的password都改成password_hash，迁移后数据库里没password_hash字段，注册登录报错
    @property # 使用装饰器@property可以把一个方法变成属性，再获取属性的值，之后可以通过接着定义@方法名.setter给属性重新赋值。如果不定义setter，则该属性值是只读的。和定义私有属性类似，但用的时候比它简单。
    def password(self):
        raise AttributeError('不能访问密码属性！')

    @password.setter
    def password(self,password): # 加密密码后再存储
        self.password_hash=generate_password_hash(password) # 默认会生成随机salt后再加密，可点击generate_password_hash()查看

    def verify_password(self,password): # 登录时校验密码
        return check_password_hash(self.password_hash,password)

    def generate_activate_token(self,expires_in=3600): # 生成用于激活帐号的token，防止激活链接中明文存放新用户的id(传id的话用user.py中注释掉的激活函数)。生成指定字典的token，键是User模型中的id，值是id的值
        s=Serializer(current_app.config['SECRET_KEY'],expires_in=expires_in) # 第二个参数是过期时间，单位秒。由于定义方法时设置了expires_in的默认值，所以调用时传不传都行
        return s.dumps({'id':self.id}) # 返回token，是一个bytes对象 from https://itsdangerous.palletsprojects.com/en/1.1.x/jws/

    @staticmethod # 帐号激活时校验token，由于激活函数(在user.py中)没有传入"User类的对象"或"新用户的id"(传id的话用user.py中注释掉的激活函数)，传的是待校验的token，所以不能直接调用校验方法，所以写成静态方法(注意没有self参数)，直接使用类名调用
    def check_activate_token(token): # 校验用于激活帐号的token
        s=Serializer(current_app.config['SECRET_KEY'])
        try:
            data=s.loads(token) # 返回生成token时的字典
        except: # token已过期，或payload被篡改导致signature验证不通过，或其他异常
            return False
        u=User.query.get(data['id'])
        if not u: # 注册后，且激活前，用户被从数据库删除，或其他导致用户不存在的异常
            return False
        if not u.is_activated:
            u.is_activated=True
            db.session.add(u)
            return True
        # else: # 如果传入一个已激活的帐号的token。这句是自己写的，可能没必要，因只要SECRET_KEY没泄漏，攻击者可能无法伪造一个可以通过服务端验证的合法token
        #     return False

    def is_collected_or_liked(self,article_id): # 一篇文章是否被当前用户收藏或点赞，参数是文章id。前端页面虽然显示收藏按钮和点赞按钮，但点击时都执行这个，没做区分，所以收藏的同时也会点赞，反之亦然，即收藏就是点赞，点赞就是收藏
        collections_and_likes=self.collections_and_likes.all() # 获取当前用户所有收藏或点赞的文章
        articles=list(filter(lambda x:x.id==int(article_id),collections_and_likes)) # 注意传来的参数是字符串类型，数据库里是int，需转换后再比较。filter()里第一个参数是执行过滤操作的函数，第二个参数是要过滤的对象，返回生成器，再转为列表
        if len(articles)>0:
            return True # 当前文章已被当前用户收藏或点赞
        return False

    def add_collect_or_like(self,article_id): # 添加收藏或点赞
        a=Article.query.get(article_id)
        a.star_times+=1
        a.collect_times+=1
        self.collections_and_likes.append(a)
        return a.collect_times # 收藏和点赞未作区分，返回谁的量都可以

    def del_collect_or_like(self,article_id): # 取消收藏或点赞
        a=Article.query.get(article_id)
        a.star_times-=1
        a.collect_times-=1
        self.collections_and_likes.remove(a)
        return a.star_times # 收藏和点赞未作区分，返回谁的量都可以


@login_manager.user_loader # 这个装饰器是个回调函数，用Flask-Login管理用户登录与退出时，根据用户id判断要登录或退出的用户是谁。[id是在用login_user()登录成功后，session里'_user_id'的值(详情在user.py中搜'_user_id')。中括号里的内容是看完下面的注释后加的]
def loader_user(uid):
    return User.query.get(uid)
'''
每次请求过来后，Flask-Login都会从Session中寻找”user_id”的值，如果找到的话，就会用这个”user_id”值来调用此回调函数，并构建一个用户类对象。因此，没有这个回调的话，Flask-Login将无法工作。
有一个问题，启用Session的话一定需要客户端允许Cookie，因为Session ID是保存在Cookie中的，如果Cookie被禁用了怎么办？
那我们的应用只好通过请求参数将用户信息带过来，一般情况下会使用一个动态的Token来表示登录用户的信息。此时，我们就不能依靠@login_manager.user_loader回调，而是使用@login_manager.request_loader回调。
from http://www.bjhee.com/flask-ext8.html，我测试的session里有'_user_id'(可打印session，也可点击login_user()查看)，他写的”user_id”不知道是不是写错了

user session 记录的是用户 ID (user_id)，回调函数的作用就是通过user_id返回对应的User对象。user_loader回调函数在user_id非法的时候不应该抛出异常，而要返回None。没有这个回调函数的话，Flask-Login将无法工作
from https://www.jianshu.com/p/01384ee741b6
'''


class Article(db.Model):
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    comment_id=db.Column(db.Integer,index=True,default=0) # 默认0表示这是文章，不是评论。不为0表示是评论，值是被评论的文章的id。index是索引，True表示允许搜索。文章和评论都有id、时间等属性，所以在文章表里添加这个字段，区分是文章还是评论
    title=db.Column(db.Text)
    content=db.Column(db.Text)
    timestamp=db.Column(db.DateTime,default=datetime.utcnow) # 文章发布时间，数据库中一般用utc时间(比东八区晚8小时，前端显示时要加8小时)，用本地时间时是datetime.now。注意utcnow后不加括号
    click_times=db.Column(db.Integer,default=0) # 点击量/阅读量
    star_times=db.Column(db.Integer,default=0) # 点赞量
    collect_times=db.Column(db.Integer,default=0) # 收藏量
    uid=db.Column(db.Integer,db.ForeignKey('user.id')) # 添加外键，关联user表的id字段，表示作者。将user表的id字段作为article表的主键。可在pycharm的Database里右键点表名-Diagrams-Show Visaulisation，查看外键关系图。
    type_id=db.Column(db.Integer,db.ForeignKey('article_type.id'),nullable=False) # 类名(ArticleType)是大驼峰时，表名转为全小写，并用下划线连接


# 收藏和点赞模型，关联user表的id和article表的id，除了手动操作数据，不需维护。第一个参数是表名，第二和第三个参数是表中的字段。也可以建成模型，即像上面的类那样，以类的形式写，但文档不建议这么做，from http://www.pythondoc.com/flask-sqlalchemy/models.html#many-to-many
collect_and_like=db.Table('collect_and_like',
                          db.Column('user_id',db.Integer,db.ForeignKey('user.id')),
                          db.Column('article_id',db.Integer,db.ForeignKey('article.id'))
                          )


class ArticleType(db.Model): # 表名是将大驼峰的类名转为全小写，并用下划线连接，如类名UserModel对应的表名是user_model。也可用__tablename__定义表名
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    type_name=db.Column(db.String(10),nullable=False)
    articles=db.relationship('Article',backref='article_type',lazy='dynamic') # 根据分类查文章。文章对象.article_type是获取对应的文章类型对象，再接着.type_name是根据文章获取文章类型